package com.hrsaas.ext.spider.core;

import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import com.hrsaas.ext.spider.util.Assert;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.ssl.SslContext;
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.util.AsciiString;
import io.netty.util.concurrent.EventExecutor;

/**
 * <p>
 * 功能：httpclient options
 * <p>
 * 
 * @author wangxiaoliang@kanzhun.com
 *
 *         2018年8月1日
 */
public final class HttpClientBuilder {

	private static final int DEFAULT_THREAD_COUNT = 4;
	private int threadCount = -1;
	private int maxChunkSize = 65_536;
	private boolean compression = false;
	private int maxInitialLineLength = 2_048;
	private int maxHeadersSize = 16_384;
	private boolean followRedirects = true;
	private CharSequence userAgent;
	private final List<RequestInterceptor> interceptors = new LinkedList<>();
	private CookieStore cookies;
	private Duration timeout;
	private SslContext sslContext;
	private AddressResolverGroup<? extends SocketAddress> resolver;
	private NioEventLoopGroup group;
	private int maxRedirects = -1;

	/**
	 * Set the SSL context to use when accessing HTTPS addresses. In particular, use
	 * this if you want to abort untrusted connections.
	 *
	 * @param ctx The context
	 * @return this
	 */
	public HttpClientBuilder setSslContext(SslContext ctx) {
		this.sslContext = ctx;
		return this;
	}

	/**
	 * 全局请求超时时间设定
	 * @param timeout The timeout, or null for no timeout (the default)
	 * @return This
	 */
	public HttpClientBuilder setTimeout(Duration timeout) {
		this.timeout = timeout;
		return this;
	}

	/**
	 * 是否接受重写向操作
	 * @return
	 */
	public HttpClientBuilder followRedirects() {
		followRedirects = true;
		return this;
	}

	/**
	 * Turn off following of redirects
	 *
	 * @return this
	 */
	public HttpClientBuilder dontFollowRedirects() {
		followRedirects = false;
		return this;
	}

	/**
	 * The number of worker threads for processing requests and responses. Netty is
	 * asynchronous, so you do not need as many threads as you will have
	 * simultaneous requests; the default is 4. Best to see if you have problems,
	 * and increase this value only if it makes a measurable improvement in
	 * throughput.
	 *
	 * @param count The number of threads
	 * @return this
	 */
	public HttpClientBuilder threadCount(int count) {
		Assert.nonNegative("threadCount", count);
		Assert.nonZero("threadCount", count);
		if (group != null) {
			throw new IllegalStateException(
					"Cannot set threadCount if you are" + " providing the NioEventLoopGroup");
		}
		this.threadCount = count;
		return this;
	}

	/**
	 * The maximum size of a chunk in bytes. The default is 64K.
	 *
	 * @param bytes A number of bytes
	 * @return this
	 */
	public HttpClientBuilder maxChunkSize(int bytes) {
		Assert.nonNegative("bytes", bytes);
		Assert.nonZero("bytes", bytes);
		this.maxChunkSize = bytes;
		return this;
	}

	/**
	 * Set the maximum length of the HTTP initial line, e.g.
	 * <code>HTTP/1.1 GET /path/to/something</code>. Unless you will be sending
	 * extremely long URLs, the default of 2048 should be plenty.
	 *
	 * @param max
	 * @return this
	 */
	public HttpClientBuilder maxInitialLineLength(int max) {
		Assert.nonZero("max", max);
		Assert.nonNegative("max", max);
		maxInitialLineLength = max;
		return this;
	}

	/**
	 * Set the maximum size of headers in bytes
	 *
	 * @return this
	 */
	public HttpClientBuilder maxHeadersSize(int max) {
		Assert.nonZero("max", max);
		Assert.nonNegative("max", max);
		maxHeadersSize = max;
		return this;
	}

	/**
	 * Turn on HTTP gzip or deflate compression
	 *
	 * @return this
	 */
	public HttpClientBuilder useCompression() {
		compression = true;
		return this;
	}

	/**
	 * Turn off HTTP gzip or deflate compression
	 *
	 * @return this
	 */
	public HttpClientBuilder noCompression() {
		compression = false;
		return this;
	}

	/**
	 * For test environments using multiple host names - attaches a DNS resolver
	 * that will resolve all host name addresses to
	 * <code>InetAddress.getLocalHost()</code>.
	 *
	 * @return this
	 */
	public HttpClientBuilder resolveAllHostsToLocalhost() {
		return resolver(new LocalhostOnlyAddressResolverGroup());
	}

	/**
	 * Set the DNS resolver to use, bypassing the default one.the passed resolver
	 * will be used to resolve <i>all</i> host names by the resulting HttpClient.
	 *
	 * @param resolver The addresss setEventLoopGroup resolver
	 * @return this
	 */
	public HttpClientBuilder resolver(AddressResolverGroup<? extends SocketAddress> resolver) {
		this.resolver = resolver;
		return this;
	}

	/**
	 * Set the DNS resolver to use, bypassing the default one.the passed resolver
	 * will be used to resolve <i>all</i> host names by the resulting HttpClient.
	 *
	 * @param          <T> The type of address the resolver resolves
	 * @param resolver The addresss setEventLoopGroup resolver
	 * @return this
	 */
	public <T extends SocketAddress> HttpClientBuilder resolver(AddressResolver<T> resolver) {
		return resolver(new OneResolverGroup<>(resolver));
	}

	/**
	 * Set the maximum number of redirects this client can encounter before it
	 * considers itself to be in a redirect loop and cancels the request, sending a
	 * cancelled event.
	 *
	 * @param maxRedirects The maximum number of redirects
	 * @return this
	 */
	public HttpClientBuilder setMaxRedirects(int maxRedirects) {
		Assert.nonNegative("maxRedirects", maxRedirects);
		this.maxRedirects = maxRedirects;
		return this;
	}

	private static final class OneResolverGroup<T extends SocketAddress> extends AddressResolverGroup<T> {

		private final AddressResolver<T> singleResolver;

		OneResolverGroup(AddressResolver<T> singleResolver) {
			this.singleResolver = singleResolver;
		}

		@Override
		protected AddressResolver<T> newResolver(EventExecutor ee) throws Exception {
			return singleResolver;
		}
	}

	/**
	 * Set the thread pool used to perform network I/O.
	 *
	 * @param group The thread pool to use
	 * @return this
	 */
	public HttpClientBuilder setEventLoopGroup(NioEventLoopGroup group) {
		Assert.notNull("group", group);
		if (threadCount != -1) {
			throw new IllegalStateException("Thread count already set. If you want to provide "
					+ "your own NioEventLoopGroup, don't also set that - these options are "
					+ "mutually exclusive.");
		}
		this.group = group;
		return this;
	}

	/**
	 * 构建netty http client
	 * @return an http client
	 */
	public HttpClient create() {
		return new HttpClient(compression, maxChunkSize, threadCount == -1 ? DEFAULT_THREAD_COUNT : threadCount,
				maxInitialLineLength, maxHeadersSize, followRedirects, userAgent, interceptors,
				Collections.unmodifiableList(new ArrayList<>(settings)), cookies,
				timeout, sslContext, resolver, group, maxRedirects);
	}

	/**
	 * Set the user agent
	 *
	 * @param userAgent
	 * @return this
	 */
	public HttpClientBuilder setUserAgent(String userAgent) {
		if (!StandardCharsets.US_ASCII.newEncoder().canEncode(userAgent)) {
			throw new IllegalArgumentException("User agent string contains non-ascii "
					+ "characters which cannot be encoded in HTTP headers");
		}
		this.userAgent = AsciiString.of(userAgent);
		return this;
	}

	/**
	 * Add an interceptor which should get a chance to process every request before
	 * it is invoked; useful for things that sign requests and such.
	 *
	 * @param interceptor An interceptor
	 * @return this
	 */
	public HttpClientBuilder addRequestInterceptor(RequestInterceptor interceptor) {
		this.interceptors.add(interceptor);
		return this;
	}

	private final List<ChannelOptionSetting<?>> settings = new LinkedList<>();

	/**
	 * Set a low-level setting for the Netty pipeline. See the
	 * <a href="http://netty.io/4.0/api/io/netty/channel/ChannelOption.html">Netty
	 * documentation</a> for what these are.
	 *
	 * @param        <T> The type
	 * @param option The applyOption
	 * @param value  The value type
	 * @return this
	 */
	public <T> HttpClientBuilder setChannelOption(ChannelOption<T> option, T value) {
		for (Iterator<ChannelOptionSetting<?>> it = settings.iterator(); it.hasNext();) {
			ChannelOptionSetting setting = it.next();
			if (setting.equals(option)) {
				it.remove();
			}
		}
		settings.add(new ChannelOptionSetting<>(option, value));
		return this;
	}

	/**
	 * Set a cookie store which will be used for all HTTP requests on the resulting
	 * HttpClient (unless overriddeen in RequestBuilder).
	 *
	 * @param store A cookie store
	 * @return this
	 */
	public HttpClientBuilder setCookieStore(CookieStore store) {
		this.cookies = store;
		return this;
	}

	/**
	 * Encapsulates a setting that can be set on the Netty Bootstrap; not really an
	 * API class, but exposed so that the HttpClient constructor can be invoked
	 * directly if someone wants to (using
	 * <a href="HttpClientBuilder.html">HttpClientBuilder</a> is much easier).
	 *
	 * @param <T> A type
	 */
	protected static final class ChannelOptionSetting<T> {

		private final ChannelOption<T> option;
		private final T value;

		ChannelOptionSetting(ChannelOption<T> option, T value) {
			this.option = option;
			this.value = value;
		}

		public ChannelOption<T> option() {
			return option;
		}

		public T value() {
			return value;
		}

		void apply(Bootstrap bootstrap) {
			bootstrap.option(option, value);
		}
	}

}
